/**
 * Licensed under the Apache License Version 2.0.
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package net.interknit.knitter.cg.model;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.interknit.knitter.cg.CGMain;

/**
 *
 */
public class JavaTypeInspector
{
	private final JavaType javaType;

	public JavaTypeInspector(JavaType javaType)
	{
		this.javaType = javaType;
	}

	/**
	 * Check if the underlying type is a concrete type (i.e., a type that is not a Java interface and not an
	 * abstract class). Note, that a concrete type does not necessarily mean an instantiable type. There are
	 * certain types which are concrete, but cannot be instantiated (for example, an Enum type).
	 *
	 * @return true if the underlying type is a concrete type. false, otherwise.
	 */
	public boolean isConcrete()
	{
		return
		! Modifier.isAbstract(javaType.getTypeClass().getModifiers()) &&
		! Modifier.isInterface(javaType.getTypeClass().getModifiers());
	}

	public boolean isVoidType()
	{
		return
		javaType.getTypeClass() == null ||
		javaType.getTypeClass().getName().equals("void") ||
		javaType.getTypeClass().getName().equals("java.lang.Void");
	}

	/**
	 * Check if the underlying type represents one of the "basic" types. The following are considered the
	 * basic types:<br/>
	 * <br/>
	 * <code>void</code> or <code>java.lang.Void</code><br/>
	 * <code>boolean</code> or <code>java.lang.Boolean</code><br/>
	 * <code>byte</code> or <code>java.lang.Byte</code><br/>
	 * <code>char</code> or <code>java.lang.Character</code><br/>
	 * <code>short</code> or <code>java.lang.Short</code><br/>
	 * <code>int</code> or <code>java.lang.Integer</code><br/>
	 * <code>float</code> or <code>java.lang.Float</code><br/>
	 * <code>long</code> or <code>java.lang.Long</code><br/>
	 * <code>double</code> or <code>java.lang.Double</code><br/>
	 * <code>java.lang.Number</code><br/>
	 * <code>java.math.BigInteger</code><br/>
	 * <code>java.math.BigDecimal</code><br/>
	 * <code>java.lang.String</code><br/>
	 * <code>java.lang.StringBuilder</code><br/>
	 * <code>java.lang.StringBuffer</code><br/>
	 * <code>java.util.Date</code><br/>
	 * <code>java.sql.Date</code><br/>
	 * <code>java.sql.Timestamp</code><br/>
	 *
	 * @return true if the underlying type is one of the basic types. false, otherwise.
	 */
	public boolean isPrimitiveType()
	{
		return
		! isArrayType() &&
		(
				// primitives
				isOfType(javaType.getTypeClass(), void.class) ||
				isOfType(javaType.getTypeClass(), boolean.class) ||
				isOfType(javaType.getTypeClass(), byte.class) ||
				isOfType(javaType.getTypeClass(), char.class) ||
				isOfType(javaType.getTypeClass(), short.class) ||
				isOfType(javaType.getTypeClass(), int.class) ||
				isOfType(javaType.getTypeClass(), float.class) ||
				isOfType(javaType.getTypeClass(), long.class) ||
				isOfType(javaType.getTypeClass(), double.class) ||
				isOfType(javaType.getTypeClass(), Void.class)
		);
	}

	public boolean isBasicType()
	{
		return
		! isArrayType() &&
		(
				javaType.getTypeClass().isPrimitive() ||
				// boxed primitives
				isOfType(javaType.getTypeClass(), Boolean.class) ||
				isOfType(javaType.getTypeClass(), Byte.class) ||
				isOfType(javaType.getTypeClass(), Character.class) ||
				isOfType(javaType.getTypeClass(), Short.class) ||
				isOfType(javaType.getTypeClass(), Integer.class) ||
				isOfType(javaType.getTypeClass(), Float.class) ||
				isOfType(javaType.getTypeClass(), Long.class) ||
				isOfType(javaType.getTypeClass(), Double.class) ||

				// string and string holders
				isOfType(javaType.getTypeClass(), String.class) ||
				isOfType(javaType.getTypeClass(), StringBuilder.class) ||
				isOfType(javaType.getTypeClass(), StringBuffer.class)
		);
	}

	public boolean isBigIntegerType()
	{
		return
		! isArrayType() &&
		(
				isOfType(javaType.getTypeClass(), java.math.BigInteger.class)
		);
	}

	public boolean isBigDecimalType()
	{
		return
		! isArrayType() &&
		(
				isOfType(javaType.getTypeClass(), java.math.BigDecimal.class)
		);
	}

	public boolean isDateTimeType()
	{
		return
		! isArrayType() &&
		(
				// date and time
				isOfType(javaType.getTypeClass(), java.sql.Timestamp.class) ||
				isOfType(javaType.getTypeClass(), java.sql.Date.class) ||
				isOfType(javaType.getTypeClass(), java.util.Date.class)
		);
	}

	public boolean isFileType()
	{
		return
		! isArrayType() && isOfType(javaType.getTypeClass(), java.io.File.class);
	}

	public boolean isInputStreamType()
	{
		return
		! isArrayType() && isOfType(javaType.getTypeClass(), java.io.InputStream.class);
	}

	public boolean isReaderType()
	{
		return
		! isArrayType() && isOfType(javaType.getTypeClass(), java.io.Reader.class);
	}

	public boolean isAnyType()
	{
		return
		! isArrayType() && javaType.getTypeClass().equals(Object.class);
	}

	public boolean isKnownType()
	{
		return
		isVoidType() ||
		isPrimitiveType() ||
		isBasicType() ||
		isDateTimeType() ||
		isAnyType() ||
		isCollectionType() ||
		isMapType() ||
		isListType() ||
		isSetType();
	}

	/**
	 * Check if this JavaType represents an array type.
	 *
	 * @return true if this JavaType is an array type. false, otherwise.
	 */
	public boolean isArrayType()
	{
		return javaType.getArrayDimensions() > 0;
	}

	/**
	 * Check if the underlying type is an implementation of java.util.Set interface which also maintains the
	 * same number of type parameters as defined by java.util.Set interface. In other words, if JavaType
	 * represents a type that implements java.util.Set but does not define the same number of type parameters,
	 * then this JavaType is not considered of type Set.
	 *
	 * @return true if the underlying type is of java.util.Set type. false, otherwise.
	 */
	public boolean isSetType()
	{
		return ! javaType.isSeedType() && ! isArrayType() && isOfType(javaType.getTypeClass(), Set.class);
	}

	/**
	 * Check if the underlying type is an implementation of java.util.List interface which also maintains the
	 * same number of type parameters as defined by java.util.List interface. In other words, if JavaType
	 * represents a type that implements java.util.List but does not define the same number of type parameters,
	 * then this JavaType is not considered of type List.
	 *
	 * @return true if the underlying type is of java.util.List type. false, otherwise.
	 */
	public boolean isListType()
	{
		return ! javaType.isSeedType() && ! isArrayType() && isOfType(javaType.getTypeClass(), List.class);
	}

	/**
	 * Check if the underlying type is an implementation of java.util.Collection interface which also maintains the
	 * same number of type parameters as defined by java.util.Collection interface. In other words, if JavaType
	 * represents a type that implements java.util.Collection but does not define the same number of type parameters,
	 * then this JavaType is not considered of type Collection.
	 *
	 * @return true if the underlying type is of java.util.Collection type. false, otherwise.
	 */
	public boolean isCollectionType()
	{
		return ! javaType.isSeedType() && ! isArrayType() && isOfType(javaType.getTypeClass(), Collection.class);
	}

	/**
	 * Check if the underlying type is an implementation of java.util.Map interface which also maintains the
	 * same number of type parameters as defined by java.util.Map interface. In other words, if JavaType
	 * represents a type that implements java.util.Map but does not define the same number of type parameters,
	 * then this JavaType is not considered of type Map.
	 *
	 * @return true if the underlying type is of java.util.Map type. false, otherwise.
	 */
	public boolean isMapType()
	{
		return ! javaType.isSeedType() && ! isArrayType() && isOfType(javaType.getTypeClass(), Map.class);
	}

	/**
	 * Check if the underlying type is an enum type (i.e., an implicit subclass of java.lang.Enum)
	 *
	 * @return true if the underlying is an enum type. false, otherwise.
	 */
	public boolean isEnumType()
	{
		return ! isArrayType() && Enum.class.isAssignableFrom(javaType.getTypeClass());
	}

	public boolean isCustomType()
	{
		return
		! isDateTimeType() &&
		! isArrayType() &&
		! isCollectionType() &&
		! isMapType() &&
		! isEnumType() &&
		! isJavaTypeIgnored();
	}

	public boolean hasPublicNoArgConstructor()
	{
		try
		{
			Constructor<?> c = javaType.getTypeClass().getConstructor();
			return c != null;
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean hasPublicStringConstructor()
	{
		try
		{
			Constructor<?> c = javaType.getTypeClass().getConstructor(String.class);
			return c != null;
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean hasFromStringMethod()
	{
		try
		{
			Method m = javaType.getTypeClass().getMethod("fromString", String.class);
			return m != null;
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean hasStaticFromStringMethod()
	{
		try
		{
			Method m = javaType.getTypeClass().getMethod("fromString", String.class);
			return
			m != null &&
			Modifier.isStatic(m.getModifiers()) &&
			m.getReturnType() != null &&
			! m.getReturnType().toString().equals("void");
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean hasMethod(boolean staticType, boolean mustHaveReturn, String methodName, Class<?>... paramTypes)
	{
		try
		{
			Method m = javaType.getTypeClass().getMethod(methodName, paramTypes);

			if(! staticType)
			{
				if(mustHaveReturn)
				{
					return
					m.getReturnType() != null &&
					! m.getReturnType().toString().equals("void");
				}
				else
				{
					return true;
				}
			}
			else
			{
				if(mustHaveReturn)
				{
					return
					m.getReturnType() != null &&
					! m.getReturnType().toString().equals("void") &&
					Modifier.isStatic(m.getModifiers());
				}
				else
				{
					return Modifier.isStatic(m.getModifiers());
				}
			}
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean hasValueOfMethod()
	{
		try
		{
			return javaType.getTypeClass().getMethod("valueOf", String.class) != null;
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean hasStaticValueOfMethod()
	{
		try
		{
			Method m = javaType.getTypeClass().getMethod("valueOf", String.class);

			return
			m != null &&
			Modifier.isStatic(m.getModifiers()) &&
			m.getReturnType() != null &&
			! m.getReturnType().toString().equals("void");
		}
		catch(NoSuchMethodException e)
		{
			return false;
		}
	}

	public boolean isJavaTypeIgnored()
	{
		return CGMain.getInstance().getCGConfig().isTypeIgnored(javaType.getTypeClass());
	}

	/**
	 * Determine if clazz parameter is of type represented by ofType parameter. For example, java.lang.Double
	 * is considered of type java.lang.Number. Similarly, isOfType(HashSet.class, Set.class) will return true.
	 * This is slightly different from the instanceof clause, which compares the runtime instances. This
	 * method compares the classes for assignability. <br>
	 * <br>
	 * This method will return false if the type parameters count of clazz and ofType do not match. For
	 * example, isOfType(Hashtable.class, Map.class) returns true because Hashtable is of type Map and
	 * Hashtable.class.getTypeParameters().length returns 2, which the same value as
	 * Map.class.getTypeParameters().length. However, isOfType(Properties.class, Map.class) will return false
	 * because, although, Properties.class is of type Map.class, their type paramters count are different
	 * (Properties.class.getTypeParameters.length returns 0).
	 *
	 * @param clazz
	 *            The clazz to be checked for
	 * @param ofType
	 *            the type for which clazz parameter will be checked for
	 * @return true if clazz is of type ofType. false, otherwise
	 */
	private static boolean isOfType(Class<?> clazz, Class<?> ofType)
	{
		return
		ofType.isAssignableFrom(clazz) &&
		clazz.getTypeParameters().length == ofType.getTypeParameters().length;
	}
}
